﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Windows;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using Microsoft.Phone.Tasks;
using WindowsPhoneMvp.Messaging;
using WindowsPhoneMvp.Navigation;
using WindowsPhoneMvp.Phone;
using WindowsPhoneMvp.Phone.Lifecycle;
using WindowsPhoneMvp.Phone.State;
using WindowsPhoneMvp.Util;
using WindowsPhoneEssentials.Diagnostics;
using WindowsPhoneEssentials.Tasks;
using WindowsPhoneEssentials.Storage;

// Only until we finally kill ReleaseView for good
#pragma warning disable 618

namespace WindowsPhoneMvp
{
    /// <summary>
    /// Represents a presenter in a Windows Phone Model-View-Presenter application.
    /// </summary>
    /// <typeparam name="TView">The type of the view.</typeparam>
    public abstract class Presenter<TView> : IPresenter<TView>, INavigatingPresenter, 
        IActivationAware, IObsuredAware, ITracingPresenter, IViewLifecycleManager
        where TView : class, IView
    {
        internal IList<TransientPropertyResolver> TransientProperties = new List<TransientPropertyResolver>();
        ITraceAppender ITracingPresenter.TraceContext { get; set; }

        /// <summary>
        /// Gets the view instance that this presenter is bound to.
        /// </summary>
        public TView View { get; private set; }

        /// <summary>
        /// Gets or sets the message bus used for cross presenter messaging.
        /// </summary>
        public IMessageBus Messages { get; set; }

        private INavigationService navigate;

        /// <summary>
        /// Navigation Service
        /// </summary>
        public INavigationService Navigate
        {
            get
            {
                return navigate;
            }
            set
            {
                if(value == null)
                    throw new ArgumentNullException("value", "Navigate cannot be set to null.");
                navigate = value;
            }
        }

        /// <summary>
        /// Navigation Parameters that have been passed to this Presenter
        /// </summary>
        public IDictionary<string, string> Params { get; set; }

        private IStorageProvider transientState;

        /// <summary>
        /// Access to the Page level Transient State Bag
        /// </summary>
        public IStorageProvider TransientState
        {
            get
            {
                if(transientState == null)
                    throw new NotSupportedException("Page TransientState is not available in this context");
                return transientState;
            }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value", "TransientState cannot be set to null.");
                transientState = value;
            }
        }

        private IStorageProvider applicationTransientState;

        /// <summary>
        /// Access to the Application level Transient State Bag
        /// </summary>
        public IStorageProvider ApplicationTransientState
        {
            get { return applicationTransientState; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value", "ApplicationTransientState cannot be set to null.");
                applicationTransientState = value;
            }
        }

        public ITaskManager TaskManager { get; set; }

        protected Presenter(TView view)
        {
            if (view == null)
                throw new ArgumentNullException("view");

            InitializeDefaultModel(view);
            View = view;

            ApplicationTransientState = new TransientStore();
            if (view is ISupportsState)
                TransientState = new PageTransientStateStore((ISupportsState) view);
            else
                TransientState = new TransientStore();
            TaskManager = new PhoneTaskManager();
        }

        protected Presenter(TView view, INavigationService navigationService)
            : this(view)
        {
            if (view == null)
                throw new ArgumentNullException("view");
            if(navigationService == null)
                throw  new ArgumentNullException("navigationService");

            Navigate = navigationService;
        }

        /// <summary>
        /// Specifies a path to a class that requires transient state on a property
        /// </summary>
        /// <typeparam name="TClassOn">Type of class that has the transient property</typeparam>
        /// <param name="path">Func that returns the class instance, eg. () => View.MyProperty</param>
        /// <returns>TransientBuilder which will specify the property</returns>
        protected TransientBuilder<TClassOn> Transient<TClassOn>(Func<TClassOn> path)
            where TClassOn : class
        {
            return new TransientBuilder<TClassOn>(this, path);
        }

        #region TransientBuilder
        ///<summary>
        /// Builds transient properties
        ///</summary>
        public class TransientBuilder<TClass>
            where TClass : class
            //where TView : class, IView
        {
            private readonly Presenter<TView> presenter;
            private readonly Delegate @delegate;

            internal TransientBuilder(Presenter<TView> presenter, Func<TClass> classFunc)
            {
                this.presenter = presenter;
// ReSharper disable ConvertClosureToMethodGroup
                Func<TClass> wrapper = () => classFunc();
// ReSharper restore ConvertClosureToMethodGroup
                @delegate = Delegate.CreateDelegate(wrapper.GetType(), wrapper.Target, wrapper.Method, true);
            }

            /// <summary>
            /// Specifies the property to be transiently persisted (on activated and deactivated events)
            /// </summary>
            /// <param name="property">Path to property, eg. x => x.MyProperty</param>
            public void Property(Expression<Func<TClass, object>> property)
            {
                var propertyname = Util.Property.For(property);
                if (!property.Type.IsPublic) throw new InvalidTransientPropertyException(propertyname, "must be public");

                var propInfo = Util.Property.InfoFor(property);
                if (propInfo.GetGetMethod() == null) throw new InvalidTransientPropertyException(typeof(TClass).FullName + "." + propertyname, "must have public get method");
                if (propInfo.GetSetMethod() == null) throw new InvalidTransientPropertyException(typeof(TClass).FullName + "." + propertyname, "must have public set method");

                presenter.TransientProperties.Add(new TransientPropertyResolver
                {
                    Delegate = @delegate,
                    PropertyName = propertyname
                });
            }
        }
        #endregion

        ///<summary>
        /// Starts a chooser
        ///</summary>
        ///<param name="onComplete">Delegate to call when chooser completes</param>
        ///<typeparam name="TChooser">Type of chooser to start</typeparam>
        ///<typeparam name="TTaskEventArgs">Type of arguments the specified chooser needs</typeparam>
        public void StartTask<TChooser, TTaskEventArgs>(Action<TTaskEventArgs> onComplete)
            where TChooser : ChooserBase<TTaskEventArgs>, new()
            where TTaskEventArgs : TaskEventArgs
        {
            TaskManager.StartTask<TChooser, TTaskEventArgs>(onComplete);
        }

        /// <summary>
        /// Dispatches an action to the UI thread if needed
        /// </summary>
        protected void Dispatch(Action action)
        {
            if (Deployment.Current.CheckAccess())
                action();
            else
                Deployment.Current.Dispatcher.BeginInvoke(action);
        }

        void IActivationAware.Activated(object sender, ActivatedEventArgs args)
        {
            //Hydrate transient properties
            foreach (var c in TransientProperties)
            {
                var resolve = c.Delegate.DynamicInvoke();
                var t = resolve.GetType();
                var propertyInfo = t.GetProperty(c.PropertyName);

                var propertyKey = string.Format("{0}.{1}", t.FullName, c.PropertyName);
                var value = ApplicationTransientState.Load<object>(propertyKey);
                propertyInfo.SetValue(resolve, value, null);
            }

            OnActivated();
        }

        void IActivationAware.Deactivated(object sender, DeactivatedEventArgs args)
        {
            //Persist transient properties
            foreach(var c in TransientProperties)
            {
                var resolve = c.Delegate.DynamicInvoke();
                var t = resolve.GetType();
                var value = t.GetProperty(c.PropertyName).GetValue(resolve, null);
                var propertyKey = string.Format("{0}.{1}", t.FullName, c.PropertyName);
                ApplicationTransientState.Save(propertyKey, value);
            }

            OnDeactivated();
        }

        /// <summary>
        /// Invoked when the presenter has been activated by the phone
        /// </summary>
        public virtual void OnActivated()
        {
            
        }

        /// <summary>
        /// Invoked when the presenter is about to be deactivated
        /// </summary>
        public virtual void OnDeactivated()
        {
            
        }

        void InitializeDefaultModel(TView view)
        {
            var viewType = view.GetType();
            if(viewType.GetCustomAttributes(typeof(InheritModelAttribute), true).Any())
                return;

            object defaultModel;
            if (GetType().GetCustomAttributes(typeof(BindAsViewModelAttribute), true).Any())
            {
                defaultModel = this;
            }
            else
            {
                var modelType = viewType
                    .GetInterfaces()
                    .Where(t => t.IsGenericType)
                    .Where(t => t.GetGenericTypeDefinition() == typeof (IView<>))
                    .Select(t => t.GetGenericArguments().Single())
                    .FirstOrDefault();
                if (modelType == null) return;
                defaultModel = Activator.CreateInstance(modelType);
            }

            view.DataContext = defaultModel;
        }

        /// <summary>
        /// Releases the view.
        /// </summary>
        [Obsolete("You are no longer required to implement ReleaseView. If you have objects that need to be disposed at the end of your presenter's lifetime, implement IDisposable instead. This method will be removed altogether in a future release.")]
        public virtual void ReleaseView()
        {
        }

        /// <summary>
        /// Called when this presenter has been navigated to
        /// </summary>
        public virtual void OnNavigationTo(NavigationData navigationData)
        {
            
        }

        ///<summary>
        /// Called when navigating away from the current presenter
        ///</summary>
        ///<param name="isExiting">True when the application is navigating to an external destination</param>
        public virtual void OnNavigatedFrom(bool isExiting)
        {
            
        }

        void IObsuredAware.Unobscured(object sender, EventArgs e)
        {
            OnUnobscured();
        }

        void IObsuredAware.Obscured(object sender, ObscuredEventArgs e)
        {
            OnObscured(e.IsLocked);
        }

        /// <summary>
        /// This event is raised when the shell chrome is covering the frame.
        /// </summary>
        /// <param name="isLocked">True if the application is obscured because the phone is now locked</param>
        public virtual void OnObscured(bool isLocked)
        {
            
        }

        /// <summary>
        /// This event is raised when the shell chrome is no longer covering the frame.
        /// </summary>
        public virtual void OnUnobscured()
        {
            
        }
    }
}
